Jakub Kosterna - praca domowa nr 1

1. Wczytanie i ogląd

Za cel pierwszej pracy domowej postanowiłem przeprowadzić eksplorację zbioru Bank Marketing.

https://www.mldata.io/dataset-details/bank_marketing/

Wpierw wczytajmy pakiety potrzebne do wykonania owego zadania.

In [1]:
import pandas as pd
import numpy as np
import sklearn
import matplotlib.pyplot as plt

Za pomocą pakietu pandas odtwórzmy i spójrzmy na nasz zbiór "na szybko".

In [2]:
data = pd.read_csv('bank_marketing_weka_dataset.csv')
data.sample(10)
Out[2]:
age job marital education default balance housing loan contact day month duration campaign pdays previous poutcome y
1468 38 management married tertiary no 0 yes no cellular 13 may 537.0 5 303.0 2 failure no
3575 51 self-employed married primary no 307 yes yes unknown 17 jun 65.0 2 -1.0 0 unknown no
284 40 student married secondary no 260 no no unknown 20 jun 197.0 6 -1.0 0 unknown no
1696 36 management single tertiary no 260 yes no cellular 7 may 49.0 1 -1.0 0 unknown no
454 44 management married tertiary no 795 no no cellular 28 aug 99.0 24 -1.0 0 unknown no
4013 39 technician married unknown no 839 no no unknown 30 may 825.0 2 -1.0 0 unknown no
1947 47 unemployed married secondary no 3188 yes no cellular 3 feb 426.0 3 -1.0 0 unknown no
4225 65 unknown married unknown no 4717 no no telephone 6 aug 256.0 1 -1.0 0 unknown yes
3952 44 blue-collar married primary no 887 yes no cellular 12 may 165.0 2 363.0 1 failure no
1236 32 admin. single secondary no 116 no yes cellular 16 jul 255.0 2 -1.0 0 unknown no
In [3]:
data.shape
Out[3]:
(4521, 17)

Nasze dane dotyczą informacji o klientach pewnego banku, a także kilku ich czynności powiązanych z ową instytucją. Możemy w nim między innymi wyczytać o ich wieku, pracy i statusie materialnym, ale także o czasu trwania ich ostatniego kontaktu z bankiem czy ilości takich przeprowadzonych rozmów.

Godne uwagi są także kolumny poutcome i y - pierwsza dotyczy sukcesu ostatniej kampanii marketingowej wobec owego klienta (?) (można spodziewać się, że idzie o kwestię, czy ostatnio subskrypcja została dokonana), zaś ta druga to spodziewana odpowiedź na pytanie czy klient dokona subskrypcji.

2. Korekta wartości nieznanych

Co ciekawe, w naszym zbiorze nie mamy do czynienia ze standardowymi wartościami NA - tu zamiast tego pojawiają się komórki "unknown". Dla wygody i świadomości istnienia funkcji właśnie pod NA, zmodyfikujmy wszystkie brakujące wartości właśnie na NA. Ale czym będzie NA w Pythonie? W sumie nie wiem, ale ten issue ze Stack Overflow: https://stackoverflow.com/questions/28654325/what-is-pythons-equivalent-of-rs-na ... sugeruje mi, że chyba NaN.

Ile w ogóle tych wartości nieznanych się znajdzie

In [4]:
data = data.replace("unknown", np.nan)
data.sample(10)
Out[4]:
age job marital education default balance housing loan contact day month duration campaign pdays previous poutcome y
2620 28 technician single secondary no 1453 yes no cellular 12 may 246.0 1 343.0 1 other no
2355 30 services married secondary no 1286 yes yes cellular 17 jul 544.0 1 -1.0 0 NaN no
667 55 management married primary no 621 no yes cellular 2 feb 114.0 1 208.0 1 failure no
1214 40 services married secondary no 0 no no NaN 12 jun 543.0 2 -1.0 0 NaN no
469 36 blue-collar single primary no 1313 yes no NaN 20 jun 8.0 10 -1.0 0 NaN no
1655 32 technician single secondary no 493 no no cellular 13 aug 289.0 2 -1.0 0 NaN no
2947 40 self-employed married tertiary no 1603 no no NaN 17 jun 279.0 1 -1.0 0 NaN no
2371 35 management divorced tertiary no 321 yes no NaN 9 may 189.0 1 -1.0 0 NaN no
4030 38 blue-collar divorced secondary no 0 yes no cellular 11 may 580.0 1 173.0 1 failure no
928 54 retired divorced tertiary no 6102 yes yes NaN 21 may 86.0 6 -1.0 0 NaN no

Ile w ogóle tych wartości nieznanych się znajdzie?

In [5]:
data.transpose().isna().mean(axis=1).reset_index()
Out[5]:
index 0
0 age 0.000000
1 job 0.008405
2 marital 0.000000
3 education 0.041363
4 default 0.000000
5 balance 0.000000
6 housing 0.000000
7 loan 0.000000
8 contact 0.292856
9 day 0.000000
10 month 0.000000
11 duration 0.000000
12 campaign 0.000000
13 pdays 0.000000
14 previous 0.000000
15 poutcome 0.819509
16 y 0.000000

Oho! Okazuje się że brakuje śladowych informacji o pracy (mniej niż 1%), kilka procent poziomu edukacji, zaś do około 3 na 10 klientów nie ma informacji o formie kontaktu i w większości nie mamy danych o tym jak zakończyła się ostatnia kampania marketingowa.

3. Eksploracja ręczna

3.1. Stan cywilny a predyktor klasy

Zacznę prosto, acz [moim zdaniem] ciekawie.

Kto prędzej sięgnie po subskrypcję? Singiel, małżonek, a może rozwodnik?

In [6]:
data_1 = data.copy()[["marital", "y"]]
data_1.sample(10)
Out[6]:
marital y
419 married no
745 married no
796 divorced no
2241 married no
3617 married no
877 single no
3876 single no
2520 divorced no
2076 married no
3684 single yes
In [7]:
data_1 = data_1.groupby(['marital','y']).size().reset_index(name = 'Count')
In [8]:
data_yes = data_1[data_1["y"] == "yes"].reset_index()
data_no = data_1[data_1["y"] == "no"].reset_index()
data_1 = data_yes
data_1["percentage"] = data_yes["Count"] / data_no["Count"] * 100
data_1[["marital", "percentage"]]
Out[8]:
marital percentage
0 divorced 17.073171
1 married 10.992063
2 single 16.229349
In [9]:
plt.bar(data_1["marital"], data_1["percentage"])

plt.xlabel('marital status') 
plt.ylabel('percentage of predicted subscribers') 
plt.title('Predictor class by civil status') 
plt.show() 

Co ciekawe bardzo podobny odsetek ludzi samotnych i rozwiedzionych jest zapowiadany na subskrypcję. Znacząco rzadziej można się spodziewać osób w związkach małżeńskich. A wydawałoby się, że to małżeństwom sprzyja częstsze branie kredytu! Na dom i wgl

3.2. Szukanie korelacji między zmiennymi

Skorzystajmy z numpy-owskiej funkcji corr() w celu wyliczenia zależności między kolumnami.

In [10]:
data.corr()
Out[10]:
age balance day duration campaign pdays previous
age 1.000000 0.083820 -0.017853 -0.002367 -0.005148 -0.008894 -0.003511
balance 0.083820 1.000000 -0.008677 -0.015950 -0.009976 0.009437 0.026196
day -0.017853 -0.008677 1.000000 -0.024629 0.160706 -0.094352 -0.059114
duration -0.002367 -0.015950 -0.024629 1.000000 -0.068382 0.010380 0.018080
campaign -0.005148 -0.009976 0.160706 -0.068382 1.000000 -0.093137 -0.067833
pdays -0.008894 0.009437 -0.094352 0.010380 -0.093137 1.000000 0.577562
previous -0.003511 0.026196 -0.059114 0.018080 -0.067833 0.577562 1.000000

W większości przypadków współczynnik korelacji okazał się być bliski zeru. Co ciekawe stosunkowo wysoką (?) korelację mają kolumny campaign i day - o tyle jest to ciekawe, że pierwsza dotyczy liczby wykonanych kontaktów, a druga dnia ostatniego.

3.3. Dzień miesiąca a łączna liczba kontaktów

Czy wynika to z tego, że dane zostały zebrane konkretnego dnia i klienci bardziej aktywni z większym prawdopodobieństwem zgadali się z bankiem także i w ostatnim czasie? Sprawdźmy to.

In [11]:
plt.scatter(data["campaign"], data["day"], s = 30)

plt.xlabel('days passed since client was last contacted') 
plt.ylabel('day of the month') 
plt.title('Predictor class by civil status') 
plt.show()

Tak sobie. Uśrednijmy wartości

In [12]:
data_2 = data.copy()[["campaign", "day"]]
data_2_1 = data_2.groupby("campaign").mean().reset_index()
data_2_2 = data_2.groupby("day").mean().reset_index()

plt.scatter(data_2_1["campaign"], data_2_1["day"])
plt.xlabel('days passed since client was last contacted') 
plt.ylabel('mean day of the month')
plt.show()
In [13]:
plt.scatter(data_2_2["day"], data_2_2["campaign"])

plt.xlabel('day of the month') 
plt.ylabel('mean number of days since last contact') 
plt.show()

Chyba możemy wnioskować z tego, że dane zostały zebrane około połowy miesiąca.

3.4. Ostatni wyniki kampanii marketingowej a predykcja kolejnego

No osobiście obstawiam, że jak klient wcześniej dokonał subskrypcji, teraz prędzej dokona - zaś ci bojaźliwi jak byli na nie, tak się nie przekonają.

In [14]:
data_3 = data.copy()[["poutcome", "y"]]
data_3.groupby(["poutcome", "y"]).size().reset_index()
Out[14]:
poutcome y 0
0 failure no 427
1 failure yes 63
2 other no 159
3 other yes 38
4 success no 46
5 success yes 83

I jest tak jak mogliśmy się spodziewać - absolutnej większości klientów, która wcześniej nie wyraziła chęci, również i teraz nie wróżymy zdecydowania się na subskrypcję, zaś dla tych co wcześniej byli na tak - w około 2/3 przypadkach liczymy na przedłużenie.

3.5. Wiek a długość ostatniej rozmowy

Strzelam, że ludzie starsi będą mieli większą tendencję do dłuższej rozmowy! Czy moja intuicja się sprawdzi? Zbadajmy to!

In [15]:
data_4 = data.copy()[["age", "duration"]]
data_4 = data_4.groupby("age").mean().reset_index()
plt.plot(data_4["age"], data_4["duration"])

plt.xlabel('client\'s age')
plt.ylabel('mean contact time') 
plt.show()

Tu akurat nie widać nic ciekawego. Coraz większe zawirowania wraz z wiekiem wynikają z małej ilości danych dla kolejnych liczb lat.

3.6. Balans konta a czas trwania ostatniej rozmowy

In [16]:
data_5 = data.copy()[["balance", "duration"]]
plt.scatter(data_5[["balance"]], data_5[["duration"]], s = 1)

plt.title('Average yearly balance in Euro by last contact duration')
plt.xlabel('yearly balance in Euro') 
plt.ylabel('last contact duration') 
plt.show()

Interesująca sprawa - wygląda na to, że wraz z większym balansem konsultacje trwają krócej. Jak widać ludzie sukcesu nie tracą czasu na głupie rozmówki!

4. Automatyczna eksploracja danych

Zacząłem się bawić pandas-em, więc chyba nic dziwnego, że zdecydowałem się na pandas-profiling. Jest to bardzo wygodne narzędzie umożliwiające szybkie zrozumienie analizowanych danych.

Zanim weźmiemy się za narzędzie od pandas-profiling, obczajmy efekt dla funkcji describe() od pandas-a.

In [17]:
data.describe()
Out[17]:
age balance day duration campaign pdays previous
count 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000
mean 41.170095 1422.657819 15.915284 263.961292 2.793630 39.766645 0.542579
std 10.576211 3009.638142 8.247667 259.856633 3.109807 100.121124 1.693562
min 19.000000 -3313.000000 1.000000 4.000000 1.000000 -1.000000 0.000000
25% 33.000000 69.000000 9.000000 104.000000 1.000000 -1.000000 0.000000
50% 39.000000 444.000000 16.000000 185.000000 2.000000 -1.000000 0.000000
75% 49.000000 1480.000000 21.000000 329.000000 3.000000 -1.000000 0.000000
max 87.000000 71188.000000 31.000000 3025.000000 50.000000 871.000000 25.000000

Niby wszystko spoko, ale co z wartościami nienumerycznymi? Przekonwertujmy je do typu numeric.

In [18]:
data_cat = data.copy()
data_cat["job"] = pd.factorize(data_cat['job'])[0] + 1
data_cat["marital"] = pd.factorize(data_cat['marital'])[0] + 1
data_cat["education"] = pd.factorize(data_cat['education'])[0] + 1
data_cat["default"] = pd.factorize(data_cat['default'])[0] + 1
data_cat["housing"] = pd.factorize(data_cat['housing'])[0] + 1
data_cat["loan"] = pd.factorize(data_cat['loan'])[0] + 1
data_cat["contact"] = pd.factorize(data_cat['contact'])[0] + 1
data_cat["month"] = pd.factorize(data_cat['month'])[0] + 1
data_cat["poutcome"] = pd.factorize(data_cat['poutcome'])[0] + 1
data_cat["y"] = pd.factorize(data_cat['y'])[0] + 1
data_cat.describe()
Out[18]:
age job marital education default balance housing loan contact day month duration campaign pdays previous poutcome y
count 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000 4521.000000
mean 41.170095 4.994913 1.498120 2.065915 1.016810 1422.657819 1.566025 1.152842 0.773723 15.915284 4.926123 263.961292 2.793630 39.766645 0.542579 0.281132 1.115240
std 10.576211 2.565034 0.695471 0.780906 0.128575 3009.638142 0.495676 0.359875 0.555248 8.247667 2.717826 259.856633 3.109807 100.121124 1.693562 0.678637 0.319347
min 19.000000 0.000000 1.000000 0.000000 1.000000 -3313.000000 1.000000 1.000000 0.000000 1.000000 1.000000 4.000000 1.000000 -1.000000 0.000000 0.000000 1.000000
25% 33.000000 3.000000 1.000000 2.000000 1.000000 69.000000 1.000000 1.000000 0.000000 9.000000 2.000000 104.000000 1.000000 -1.000000 0.000000 0.000000 1.000000
50% 39.000000 4.000000 1.000000 2.000000 1.000000 444.000000 2.000000 1.000000 1.000000 16.000000 4.000000 185.000000 2.000000 -1.000000 0.000000 0.000000 1.000000
75% 49.000000 6.000000 2.000000 3.000000 1.000000 1480.000000 2.000000 1.000000 1.000000 21.000000 8.000000 329.000000 3.000000 -1.000000 0.000000 0.000000 1.000000
max 87.000000 11.000000 3.000000 3.000000 2.000000 71188.000000 2.000000 2.000000 2.000000 31.000000 12.000000 3025.000000 50.000000 871.000000 25.000000 3.000000 2.000000

Okey, teraz kolumny niektóre nie mają sensu [patrz: chociażby miesiąc], ale jakieś informacje z tego dostaniemy.

Co w przypadku pandas_profiling?

In [19]:
import pandas_profiling
pandas_profiling.ProfileReport(data)








Out[19]:

Omg, przepiękne to jest!

Kocham pandas-profiling!

O czym się dowiedzieliśmy?

  1. Konkretna liczba brakujących komórek - 6.8%
  2. Każdy wiersz jest unikalny
  3. Dla wartości liczbowych dostaliśmy histogramy (czasem lepsze, czasem gorsze), zaś dla tekstowych - wykresy ilości
  4. Nie każda kolumna ma te same właściwości - np. balance zawiera informację o ilości zer
  5. Interakcje - mamy cudowne hex-ploty, którymi możemy się łatwo interaktywnie pobawić
  6. Korelacje - dostajemy ładne siatki informujące nas o powiązaniami między zmiennymi. Bardzo łatwo zauważyć z nich np. (z wyjątkiem tego co zauważone wcześniej), że praca jest silnia skorelowana z edukacją, miesiąc z kredytem mieszkaniowym (???) czy miesiąc z dniem [również na tym etapie tego nie rozumiem]
  7. Zostały także udostępnione proste wizualizacje występowań brakujących wartości, a także prezentacja pierwszych i ostatnich wierszy ramki danych

To by było na tyle! Ahh pandas-profilling, czuję że będę do ciebie wracał